package scheduler;

import java.util.*;

public class Scheduler {
	private class TimeTriple {
		private int period, currTime;
		private long lastExecutionTime;
		
		TimeTriple(int newPeriod) {
			this.period = (newPeriod <= 0 ? 1:newPeriod);
			currTime = 0;
			lastExecutionTime = System.nanoTime();
		}
		
		void updateTime() {
			currTime = (currTime + 1) % period;
		}
		
		boolean needsToBeExecuted() {
			return currTime == 0;
		}
		
		void updateLastExecutionTime() {
			lastExecutionTime = System.nanoTime();
		}
		
		long getLastExecutionTime() {
			return lastExecutionTime;
		}
	}
	
	private class Event {
		private Notifiable n;
		private TimeTriple t;
		
		Event(Notifiable n, int period) {
			this.n = n;
			t = new TimeTriple(period);
		}
		
		Notifiable getNotifiable() {
			return n;
		}
		
		TimeTriple getTimeTriple() {
			return t;
		}
	}
	
	private int tickLengthMS = 25;
	private static Scheduler instance = null;
	private static Scheduler persistantInstance = null;
	private boolean paused = false;
	
	private LinkedHashSet<Event> toAdd = null;
	private Map<Notifiable, TimeTriple> events = null;
	private Set<Notifiable> oneTimeEvents = null;
	private LinkedHashSet<Notifiable> toRemove = null;

	private Timer currentTimer = null;
	
	private Scheduler() {
		toAdd = new LinkedHashSet<Event>();
		events = new HashMap<Notifiable, TimeTriple>();
		oneTimeEvents = new HashSet<Notifiable>();
		toRemove = new LinkedHashSet<Notifiable>();
		startTimer();
	}
	
	public static Scheduler getPersistantClock() {
		if (persistantInstance == null) {
			persistantInstance = new Scheduler();
		}
		return (persistantInstance);
	}
	
	public static Scheduler getInstance() {
		if (instance == null)
			instance = new Scheduler();
		return instance;
	}
	
	private void startTimer() {
		currentTimer = new Timer();
		currentTimer.schedule(new TimerTask() {
			public void run() {
				synchronized(Scheduler.getInstance()) {
					if (!toAdd.isEmpty()) {
						for (Event e : toAdd) {
							events.put(e.getNotifiable(), e.getTimeTriple());
						}
					}
					toAdd.clear();
					if (!toRemove.isEmpty()) {
						for (Notifiable n : toRemove) {
							if (events.containsKey(n)) {
								events.remove(n);
								if (oneTimeEvents.contains(n))
									oneTimeEvents.remove(n);
							}
						}
					}
					toRemove.clear();
					for (Notifiable n : events.keySet()) {
						events.get(n).updateTime();
						if (events.get(n).needsToBeExecuted()) {
							n.execute();
							events.get(n).updateLastExecutionTime();
							if (oneTimeEvents.contains(n)) {
								toRemove.add(n);
							}
						}
					}
				}
			}
			
		}, tickLengthMS, tickLengthMS);
	}

	
	public void schedule(Notifiable toSchedule, int period) throws DuplicateEventException {
		synchronized(Scheduler.getInstance()) {
			if (toAdd.contains(toSchedule) || events.containsKey(toSchedule))
				throw new DuplicateEventException("Cannot schedule a duplicate event");
			toAdd.add(new Event(toSchedule, period));
		}
	}
	
	public void scheduleOnce(Notifiable toSchedule, int period) throws DuplicateEventException {
		synchronized(Scheduler.getInstance()) {
			if (toAdd.contains(toSchedule) || events.containsKey(toSchedule))
				throw new DuplicateEventException("Cannot schedule a duplicate event");
			toAdd.add(new Event(toSchedule, period));
			oneTimeEvents.add(toSchedule);
		}
	}
	
	public void cancel(Notifiable toCancel) {
		synchronized(Scheduler.getInstance()) {
			toRemove.add(toCancel);
		}
	}
	
	public void pause() {
		synchronized(Scheduler.getInstance()) {
			if (!paused) {
				paused = true;
			
				currentTimer.cancel();
				currentTimer = null;
			}
		}
	}
	
	public void resume() {
		synchronized(Scheduler.getInstance()) {
			if (paused) {
				paused = false;
			
				startTimer();
			}
		}
	}
	
	public long getMSSinceLastExecution(Notifiable n) {
		return (System.nanoTime() - events.get(n).getLastExecutionTime()) / 1000000;
	}
	
//*
 	public static void main(String[] args) {
		
		//
		// Tests Scheduler.
		// @author Uttam
		//
		Scanner in = new Scanner(System.in);
		String command;
		class Task implements Notifiable {
			String id;
			
			public Task(String s) {
				id = s;
			}
			
			public void execute() {
				System.out.println(id + " has fired!");
			}
		};
		
		Map<String, Task> tasks = new HashMap<String, Task>();
		Scheduler scheduler = Scheduler.getInstance();
		
		while (!(command = in.next().toUpperCase()).equals("EXIT")) {
			if (command.equals("ADD")) {
				String id = in.next();
				if (!tasks.containsKey(id))
					tasks.put(id, new Task(id));
				try {
					scheduler.schedule(tasks.get(id), in.nextInt());
				}
				catch (Exception e) {
					e.printStackTrace();
				}
			}
			else if (command.equals("ADDONCE")) {
				String id = in.next();
				if (!tasks.containsKey(id))
					tasks.put(id, new Task(id));
				try {
					scheduler.scheduleOnce(tasks.get(id), in.nextInt());
				}
				catch (Exception e) {
					e.printStackTrace();
				}
			}
			else if (command.equals("CANCEL")) {
				String id = in.next();
				scheduler.cancel(tasks.get(id));
				tasks.remove(id);
			}
			else if (command.equals("PAUSE")) {
				scheduler.pause();
			}
			else if (command.equals("RESUME")) {
				scheduler.resume();
			}
			else if (command.equals("GETTIME")) {
				String id = in.next();
				System.out.println(scheduler.getMSSinceLastExecution(tasks.get(id)));
			}
		}
		System.exit(0);

	}
//*/
	
}

